/*
   実行環境は
   VMware Fusion で動作する ubuntu 12.04 64bit

   リファレンスは
   http://developer.gnome.org/gtk2/2.24/
   http://www.cairographics.org/manual/

   # コンパイルは
   src="my_viewer2_gtk2"
   opt=$( pkg-config gtk+-2.0 --cflags --libs )
   g++ -o $src $src.cpp $opt

   # 実行は
   img="lenna.png"
   ./$src $img

   os X から ssh -X 使用時 表示をホストに変えるときは
   export DISPLAY=:0.0
   os X に戻すときは
   export DISPLAY=:10.0
*/

#include <iostream>
#include <gtk/gtk.h>

  struct my_data {
    int             argc ;
    char         ** argv ;

    GdkPixbuf     * navi_pixbuf  ;
    GtkWidget     * navi_canvas  ;

    GdkRectangle    navi_image   ;
    GdkRectangle    navi_buffer  ;
    GdkRectangle    navi_request ;

    GtkWidget     * navi_layout  ;
    GtkAdjustment * navi_hadjust ;
    GtkAdjustment * navi_vadjust ;
    GtkWidget     * navi_window  ;
    GtkWidget     * navi_hbox    ;

    GdkCursorType   navi_cursor         ;
    GdkPoint        navi_adjust_pressed ;
    GdkPoint        navi_mouse_pressed  ;

    GdkPixbuf     * expand_pixbuf  ;
    GtkWidget     * expand_canvas  ;

    GdkRectangle    expand_image   ;
    GdkRectangle    expand_buffer  ;
    GdkRectangle    expand_request ;

    GtkWidget     * expand_layout  ;
    GtkAdjustment * expand_hadjust ;
    GtkAdjustment * expand_vadjust ;
    GtkWidget     * expand_window  ;

    GdkCursorType   expand_cursor         ;
    GdkPoint        expand_adjust_pressed ;
    GdkPoint        expand_mouse_pressed  ;

    GtkWidget     * roi_expand_canvas   ;
    guint32         roi_expand_color    ;
    gboolean        roi_expand_selected ;

    GdkPoint        mouse_posi       ;
    GdkCursor     * mouse_cursor     ;
    GdkEventType    mouse_eventtype  ;
  } ;

  #define my_signal_connect( object , signal , callback ) ( \
    g_signal_connect( G_OBJECT( object ), ( signal ), G_CALLBACK( callback ), & data ) \
  )

  gboolean expand_scrolled(   GtkWidget * , GdkEventScroll * , my_data * );

  void roi_expand_update( my_data * );
  void moveto_roi_expand( my_data * );
  gboolean on_roi_expand( gint , gint , GdkCursorType , my_data * );
  gboolean roi_expand_key_pressed(GtkWidget * , GdkEventKey * , my_data * );

  void navi_image_update( gint , my_data * );

// ================================================================================
// navi
//   navi_update
//   navi_x_changed , navi_y_changed
//   navi_width_changed , navi_height_changed
//   navi_scrolled , navi_key_pressed
//   navi_mouse_pressed , navi_mouse_released , navi_mouse_moved
//   navi_scale_clicked
// --------------------------------------------------------------------------------

  static void navi_update(
    my_data * data
  ) {

    // move or update canvas
    if(
      data -> navi_buffer .x <= data -> navi_request .x &&
      data -> navi_buffer .y <= data -> navi_request .y &&
      data -> navi_request .x + data -> navi_request .width  <=
        data -> navi_buffer .x + data -> navi_buffer .width     &&
      data -> navi_request .y + data -> navi_request .height <=
        data -> navi_buffer .y + data -> navi_buffer .height
    ) {
      // move canvas
        gint offset_x ; gint offset_y ;
          offset_x = data -> navi_request .x - data -> navi_buffer .x ;
          offset_y = data -> navi_request .y - data -> navi_buffer .y ;
        gtk_layout_move(
          GTK_LAYOUT( data -> navi_layout ), data -> navi_canvas , - offset_x , - offset_y
        );
    }
    else {
      g_print( "navi update ..." ) ;

      gint image_width ; gint image_height ;
        image_width  = data -> navi_image .width  ;
        image_height = data -> navi_image .height ;

      // update size
        gtk_adjustment_set_upper( data -> navi_hadjust , image_width  );
        gtk_adjustment_set_upper( data -> navi_vadjust , image_height );

      // update page
        gtk_adjustment_set_page_size(      data -> navi_hadjust , data -> navi_request .width  );
        gtk_adjustment_set_page_increment( data -> navi_hadjust , data -> navi_request .width  );
        gtk_adjustment_set_page_size(      data -> navi_vadjust , data -> navi_request .height );
        gtk_adjustment_set_page_increment( data -> navi_vadjust , data -> navi_request .height );

      GtkAllocation buffer ;
        gint margin ;
          margin = 1200 ;
        buffer .x      = data -> navi_request .x      - margin ;
        buffer .y      = data -> navi_request .y      - margin ;
        buffer .width  = data -> navi_request .width  + margin ;
        buffer .height = data -> navi_request .height + margin ;

      // adjust alloc
        buffer .x = ( 0 < buffer .x ) ? buffer .x : 0 ;
        buffer .y = ( 0 < buffer .y ) ? buffer .y : 0 ;
        buffer .width  += data -> navi_request .x - buffer .x ;
        buffer .height += data -> navi_request .y - buffer .y ;
        buffer .width  = ( buffer .x + buffer .width  < image_width  ) ? buffer .width  :
                                                                         image_width  - buffer .x ;
        buffer .height = ( buffer .y + buffer .height < image_height ) ? buffer .height :
                                                                         image_height - buffer .y ;

      // update allocation
        data -> navi_buffer .x      = buffer .x      ;
        data -> navi_buffer .y      = buffer .y      ;
        data -> navi_buffer .width  = buffer .width  ;
        data -> navi_buffer .height = buffer .height ;

      // update navi_canvas
        GdkPixbuf * sub_pixbuf ;
          sub_pixbuf = gdk_pixbuf_new_subpixbuf(
            data -> navi_pixbuf , buffer .x , buffer .y , buffer .width , buffer .height
          );
        gint offset_x ; gint offset_y ;
          offset_x = data -> navi_request .x - buffer .x ;
          offset_y = data -> navi_request .y - buffer .y ;

        if( data -> navi_canvas == NULL ) {
          GtkWidget * navi_canvas ;
            navi_canvas = gtk_image_new_from_pixbuf( sub_pixbuf );
          gtk_layout_put( GTK_LAYOUT( data -> navi_layout ), navi_canvas , - offset_x , - offset_y );
          data -> navi_canvas = navi_canvas ;
        }
        else {
          gtk_image_set_from_pixbuf( GTK_IMAGE( data -> navi_canvas ), sub_pixbuf );
          gtk_layout_move(
            GTK_LAYOUT( data -> navi_layout ), data -> navi_canvas , - offset_x , - offset_y
          );
        }
        g_object_unref( sub_pixbuf );

      g_print( " %d %d ", data -> navi_request .x , data -> navi_request .y ) ;
      g_print( " x: %d %d %d", buffer .x , buffer .x + buffer .width  , offset_x ) ;
      g_print( " y: %d %d %d", buffer .y , buffer .y + buffer .height , offset_y ) ;
      g_print( "\n" ) ;
    }

    roi_expand_update( data );
    return ;
  }


  gboolean navi_x_changed(
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint val ; gint upper ; gint limit ;
      val   = gtk_adjustment_get_value( adjustment );
      upper = gtk_adjustment_get_upper( adjustment );
      limit = upper - data -> navi_request .width ;

    if( limit < val ) {
      val = limit ; gtk_adjustment_set_value( adjustment , val );
    }
    data -> navi_request .x = val ; navi_update( data );

    return FALSE ;
  }


  gboolean navi_y_changed(
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint val ; gint limit ;
      val   = gtk_adjustment_get_value( adjustment );
      limit = gtk_adjustment_get_upper( adjustment ) - data -> navi_request .height ;

    if( limit < val ) {
      val = limit ; gtk_adjustment_set_value( adjustment , val );
    }
    data -> navi_request .y = val ; navi_update( data );

    return FALSE ;
  }


  gboolean navi_width_changed (
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint upper ;
      upper = gtk_adjustment_get_upper( data -> navi_hadjust );
    gint page ;
      page = gtk_adjustment_get_page_size( adjustment );

    if( page == data -> navi_request .width ) { return FALSE ; }
    else if( page <= upper - data -> navi_request .x ) {
      data -> navi_request .width = page ;
      gtk_adjustment_set_page_size(      data -> navi_hadjust , page );
      gtk_adjustment_set_page_increment( data -> navi_hadjust , page );
      navi_update( data );
    }
    else if( page < upper ) {
      data -> navi_request .width = page ;
      gtk_adjustment_set_page_size(      data -> navi_hadjust , page );
      gtk_adjustment_set_page_increment( data -> navi_hadjust , page );
      gtk_adjustment_set_value( data -> navi_hadjust , upper - page );
    }
    else {
      gtk_adjustment_set_page_size(      data -> navi_hadjust , upper );
      gtk_adjustment_set_page_increment( data -> navi_hadjust , upper );
      data -> navi_request .width = upper ;
      if( 0 < data -> navi_request .x ) { gtk_adjustment_set_value( data -> navi_hadjust , 0 ); }
      if( data -> navi_buffer .width < upper ) { navi_update( data ); }
    }

    return FALSE ;
  }


  gboolean navi_height_changed (
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint upper ;
      upper = gtk_adjustment_get_upper( data -> navi_vadjust );
    gint page ;
      page = gtk_adjustment_get_page_size( adjustment );

    if( page == data -> navi_request .height ) { return FALSE ; }
    else if( page <= upper - data -> navi_request .y ) {
      data -> navi_request .height = page ;
      gtk_adjustment_set_page_size(      data -> navi_vadjust , page );
      gtk_adjustment_set_page_increment( data -> navi_vadjust , page );
      navi_update( data );
    }
    else if( page < upper ) {
      data -> navi_request .height = page ;
      gtk_adjustment_set_page_size(      data -> navi_vadjust , page );
      gtk_adjustment_set_page_increment( data -> navi_vadjust , page );
      gtk_adjustment_set_value( data -> navi_vadjust , upper - page );
    }
    else {
      data -> navi_request .height = upper ;
      gtk_adjustment_set_page_size(      data -> navi_vadjust , upper );
      gtk_adjustment_set_page_increment( data -> navi_vadjust , upper );
      if( 0 < data -> navi_request .y ) { gtk_adjustment_set_value( data -> navi_vadjust , 0 ); }
      if( data -> navi_buffer .height < upper ) { navi_update( data ); }
    }

    return FALSE ;
  }


  gboolean navi_scrolled (
    GtkWidget      * widget ,
    GdkEventScroll * event  ,
    my_data        * data
  ) {

    // scroll roi
      if( data -> roi_expand_selected ) { // scroll expand
        expand_scrolled( widget , event , data );
        return FALSE ;
      }

    // scroll navi
      gint k ; gboolean h ;
      switch( event -> direction ) {
        case GDK_SCROLL_UP    :  k = -1 ; h = FALSE ; break;
        case GDK_SCROLL_DOWN  :  k =  1 ; h = FALSE ; break;
        case GDK_SCROLL_LEFT  :  k = -1 ; h = TRUE  ; break;
        case GDK_SCROLL_RIGHT :  k =  1 ; h = TRUE  ; break;
        default : return FALSE ;
      }
 
      GtkAdjustment * adjust ;
        adjust = h ? data -> navi_hadjust : data -> navi_vadjust ;
 
      gint val ; gint step ; gint upper ; gint page ;
        val   = gtk_adjustment_get_value( adjust );
        step  = gtk_adjustment_get_step_increment( adjust );
        upper = gtk_adjustment_get_upper( adjust );
        page  = gtk_adjustment_get_page_size( adjust );
 
      gint val_new ;
      val_new = val + k * 4 * step ;
      val_new = ( val_new < upper - page ) ? val_new : upper - page ;
      gtk_adjustment_set_value( adjust , val_new );

    return FALSE ;
  }


  gboolean navi_key_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {

    gint p ; gint q ; gint r ; gint s ; gboolean h ;
        p = 0 ; q = 0 ; r = 1 ; s = 0 ; h = FALSE ;
    switch( event -> keyval ) {
      case 0x0020 : // space
        data -> roi_expand_selected = ! ( data -> roi_expand_selected ) ;
        roi_expand_update( data ) ;
        break ;
      case 0xff50 : r =  0 ; break ; // home
      case 0xff57 : q =  1 ; break ; // end
      case 0xff55 : p = -1 ; break ; // page up
      case 0xff56 : p =  1 ; break ; // page down
      case 0xff51 : s = -1 ; h = TRUE ; break ; // ←
      case 0xff52 : s = -1 ;            break ; // ↑
      case 0xff53 : s =  1 ; h = TRUE ; break ; // →
      case 0xff54 : s =  1 ;            break ; // ↓
      default : /*
        g_print( "key: %04x - %04x%s%s%s%s\n",
          event -> keyval ,
          event -> state ,
          event -> state & GDK_SHIFT_MASK        ? " shift"   : "" ,
          event -> state & GDK_CONTROL_MASK      ? " control" : "" ,
          event -> state & GDK_MOD2_MASK         ? " command" : "" ,
          event -> state & GDK_BUTTON5_MASK << 1 ? " option"  : ""
        ) ; // */
        return FALSE ;
    }

    // scroll roi
      if( data -> roi_expand_selected ) { // scroll expand
        roi_expand_key_pressed( widget , event , data );
        return TRUE ;
      }

    // scroll navi
      if( event -> state & GDK_SHIFT_MASK ) { return FALSE ; }

      GtkAdjustment * adjust ;
        adjust = ( s == 0 && event -> state & GDK_CONTROL_MASK || h ) ?
                   data -> navi_hadjust : data -> navi_vadjust ;
      gint val ; gint step ; gint page ; gint upper ;
        val   = gtk_adjustment_get_value( adjust );
        step  = gtk_adjustment_get_step_increment( adjust );
        page  = gtk_adjustment_get_page_increment( adjust );
        upper = gtk_adjustment_get_upper( adjust );
      gint d ;
        d = ( event -> state & GDK_CONTROL_MASK ) ? 9 * step : 1 ;
 
      gint val_new ;
        val_new = p * page + q * upper + r * val + s * d ;
        val_new = ( val_new < 0 ) ? 0 : ( val_new < upper - page ) ? val_new : upper - page ;
      gtk_adjustment_set_value( adjust , val_new );

    return FALSE ;
  }


  gboolean navi_mouse_pressed (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    // save mouse event type
      data -> mouse_eventtype = event -> type ;

    // save mouse position
      data -> mouse_posi .x = event -> x ;
      data -> mouse_posi .y = event -> y ;

    if( event -> type == GDK_BUTTON_PRESS &&
        data -> navi_cursor == GDK_BLANK_CURSOR ) { // first single click

      GdkCursorType type ;
        gint x ; gint y ;
          x = event -> x ;
          y = event -> y ;
        type =
          ! ( data -> roi_expand_selected )                       ? GDK_FLEUR :
          on_roi_expand( x , y , GDK_FLEUR , data ) ? (
          on_roi_expand( x , y , GDK_BOTTOM_RIGHT_CORNER , data ) ? GDK_BOTTOM_RIGHT_CORNER :
          on_roi_expand( x , y , GDK_RIGHT_SIDE ,  data )         ? GDK_RIGHT_SIDE   :
          on_roi_expand( x , y , GDK_BOTTOM_SIDE , data )         ? GDK_BOTTOM_SIDE  :
                                                                    GDK_FLEUR
          ) :                                                       GDK_BLANK_CURSOR ;
      if( type != GDK_BLANK_CURSOR ) {
        // change cursor
          data -> mouse_cursor = gdk_window_get_cursor( gtk_widget_get_window( widget ) );
          data -> navi_cursor  = type ;
          gdk_window_set_cursor(
            gtk_widget_get_window( widget ), gdk_cursor_new( data -> navi_cursor )
          );
        // save adjust value
          data -> navi_adjust_pressed .x =
            data -> roi_expand_selected ?
              ( type == GDK_FLEUR ?       gtk_adjustment_get_value( data -> expand_hadjust ) :
                                          data -> expand_request .width                    ) :
                                          gtk_adjustment_get_value( data -> navi_hadjust   ) ;
          data -> navi_adjust_pressed .y =
            data -> roi_expand_selected ?
              ( type == GDK_FLEUR ?       gtk_adjustment_get_value( data -> expand_vadjust ) :
                                          data -> expand_request .height                   ) :
                                          gtk_adjustment_get_value( data -> navi_vadjust   ) ;
        // save mouse position
          data -> navi_mouse_pressed .x = event -> x ;
          data -> navi_mouse_pressed .y = event -> y ;
      }

    }

    return FALSE ;
  }


  gboolean navi_mouse_released (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    // restore mouse cursor
      if( data -> navi_cursor != GDK_BLANK_CURSOR ) {
        gdk_window_set_cursor( gtk_widget_get_window( widget ), data -> mouse_cursor );
        data -> mouse_cursor = NULL ;
        data -> navi_cursor = GDK_BLANK_CURSOR ;
      }

    if( data -> mouse_eventtype == GDK_2BUTTON_PRESS ) { // double click

      if( data -> roi_expand_selected ||
          on_roi_expand( event -> x , event -> y , GDK_FLEUR , data )
      ) {
        // select roi
          data -> roi_expand_selected = TRUE ;

        gdouble k ;
          k = (gdouble) data -> navi_image .width / data -> expand_image .width ;
        gint x ; gint y ;
          x = data -> navi_request .x + data -> mouse_posi .x ;
          y = data -> navi_request .y + data -> mouse_posi .y ;
          gint x_max ; gint y_max ;
            x_max = data -> expand_image .width  - data -> expand_request .width  ;
            y_max = data -> expand_image .height - data -> expand_request .height ;
          x = ( x < x_max ) ? x : x_max ;
          y = ( y < y_max ) ? y : y_max ;

        // move roi direct
          gtk_adjustment_set_value( data -> expand_hadjust , x / k );
          gtk_adjustment_set_value( data -> expand_vadjust , y / k );
          moveto_roi_expand( data );

        // adjust mouse pointer
          gint mouse_x ; gint mouse_y ;
            mouse_x = k * data -> expand_request .x - data -> navi_request .x ;
            mouse_y = k * data -> expand_request .y - data -> navi_request .y ;
          gint mouse_dx ; gint mouse_dy ;
            mouse_dx = mouse_x - data -> mouse_posi .x ;
            mouse_dy = mouse_y - data -> mouse_posi .y ;

          if( mouse_dx != 0 && mouse_dy != 0 ) {
            GdkDisplay * display ;
              display = gdk_window_get_display( gtk_widget_get_window( widget ) );
            GdkScreen * screen ; gint mx ; gint my ;
              gdk_display_get_pointer( display , & screen , & mx , & my , NULL );
            gdk_display_warp_pointer( display , screen , mx + mouse_dx , my + mouse_dy );
          }
      }

      return FALSE ;
    }

    // select , deselect roi
      if( on_roi_expand( event -> x , event -> y , GDK_FLEUR , data ) ) {
        if( data -> roi_expand_selected ) {
          gint move_x ; gint move_y ;
            move_x = (gint) event -> x - data -> navi_mouse_pressed .x ;
            move_y = (gint) event -> y - data -> navi_mouse_pressed .y ;
          gint d ;
            d = 3 ;
          if( - d < move_x && move_x < d && - d < move_y && move_y < d ) { // mouse not moved
            // deselect roi
              data -> roi_expand_selected = FALSE ;
              roi_expand_update( data );
          }
        }
        else {
          // select roi
            data -> roi_expand_selected = TRUE ;
            roi_expand_update( data );
        }
      }

    return FALSE ;
  }


  gboolean navi_mouse_moved (
    GtkWidget      * widget ,
    GdkEventMotion * event  ,
    my_data        * data
  ) {

    if( data -> navi_cursor == GDK_BLANK_CURSOR ) { return FALSE ; }

    // mouse move
      gint move_x ; gint move_y ;
        move_x = (gint) event -> x - data -> mouse_posi .x ;
        move_y = (gint) event -> y - data -> mouse_posi .y ;
      gint d ;
        d = 3 ;

    if( - d < move_x && move_x < d && - d < move_y && move_y < d ) {} else { // mouse moved

      gint mouse_x ; gint mouse_y ;
        GdkWindow * window ;
          window = gtk_widget_get_window( data -> navi_window );
        gint ox ; gint oy ;
          gdk_window_get_position( window , & ox , & oy );
        GdkDisplay * display ;
          display = gdk_window_get_display( window );
        gdk_display_get_pointer( display , NULL , & mouse_x , & mouse_y , NULL );
          mouse_x -= ox ;
          mouse_y -= oy ;

      if( data -> roi_expand_selected ) {

        gdouble k ;
          k = (gdouble) data -> navi_image .width / data -> expand_image .width ;

        if( data -> navi_cursor == GDK_FLEUR ) { // drag expand

          gint x ; gint y ;
            x = data -> navi_adjust_pressed .x + ( mouse_x - data -> navi_mouse_pressed .x ) / k ;
            y = data -> navi_adjust_pressed .y + ( mouse_y - data -> navi_mouse_pressed .y ) / k ;

          gint x_max ; gint y_max ;
            x_max = data -> expand_image .width  - data -> expand_request .width  ;
            y_max = data -> expand_image .height - data -> expand_request .height ;

          gtk_adjustment_set_value( data -> expand_hadjust , x < x_max ? x : x_max );
          gtk_adjustment_set_value( data -> expand_vadjust , y < y_max ? y : y_max );

        }
        else { // resize expand

          gint dw ; gint dh ;
            dw = ( mouse_x - data -> navi_mouse_pressed .x ) / k ;
            dh = ( mouse_y - data -> navi_mouse_pressed .y ) / k ;

          gint w ; gint h ;
            w = data -> navi_adjust_pressed .x ;
            h = data -> navi_adjust_pressed .y ;

          gint w_max ; gint h_max ; gint w_min ; gint h_min ;
            w_max = data -> expand_image .width  - data -> expand_request .x ;
            h_max = data -> expand_image .height - data -> expand_request .y ;
            w_min = 50 ;
            h_min = 50 ;

          gint w_new ; gint h_new ;
            w_new = w ;
            h_new = h ;

          if( data -> navi_cursor == GDK_RIGHT_SIDE ||
              data -> navi_cursor == GDK_BOTTOM_RIGHT_CORNER ) {
            w_new = ( w + dw < w_max ) ? w + dw : w_max ;
            if( w_new < w_min ) { w_new = w_min ; }
          }
          if( data -> navi_cursor == GDK_BOTTOM_SIDE ||
              data -> navi_cursor == GDK_BOTTOM_RIGHT_CORNER ) {
            h_new = ( h + dh < h_max ) ? h + dh : h_max ;
            if( h_new < h_min ) { h_new = h_min ; }
          }

          gtk_window_resize( GTK_WINDOW( data -> expand_window ), w_new , h_new );

        }

      }
      else {

        if( data -> navi_cursor == GDK_FLEUR ) { // drag navi
          gint x ; gint y ;
            x = data -> navi_adjust_pressed .x - ( mouse_x - data -> navi_mouse_pressed .x );
            y = data -> navi_adjust_pressed .y - ( mouse_y - data -> navi_mouse_pressed .y );
            gint x_max ; gint y_max ;
              x_max = data -> navi_image .width  - data -> navi_request .width  ;
              y_max = data -> navi_image .height - data -> navi_request .height ;
            x = ( x < x_max ) ? x : x_max ;
            y = ( y < y_max ) ? y : y_max ;
          gtk_adjustment_set_value( data -> navi_hadjust , x );
          gtk_adjustment_set_value( data -> navi_vadjust , y );
        }

      }

      // save mouse position
        data -> mouse_posi .x = mouse_x ;
        data -> mouse_posi .y = mouse_y ;
    }

    return FALSE ;
  }


void navi_scale_clicked (
    GtkWidget * widget ,
    my_data   * data
  ) {

    GtkAllocation win_size ;
      gtk_widget_get_allocation( data -> navi_hbox , & win_size );

    gint expand_width ; gint expand_height ;
      expand_width  = data -> expand_image .width ;
      expand_height = data -> expand_image .height ;

    gint navi_width ;
      navi_width = data -> navi_image .width ;

    gint width_req ;
      std::string label ;
        label = gtk_button_get_label( GTK_BUTTON( widget ) ) ;
      gdouble k ;
        k = (gdouble) expand_width / expand_height ;
      width_req =
        label .compare( "⇔" ) == 0 ? win_size .width :
        label .compare( "⇕" ) == 0 ? gint( k * win_size .height ) :
        label .compare( "＋" ) == 0 ? navi_width + 150 :
        label .compare( "ー" ) == 0 ? navi_width - 150 :
                                      1200 ;
      width_req =
        width_req < 500          ?  500  :
        width_req > 2000         ?  2000 :
        width_req > expand_width ?  expand_width :
                                    width_req ;

    navi_image_update( width_req , data );

    data -> navi_buffer .x = 0 ;
    data -> navi_buffer .y = 0 ;
    data -> navi_buffer .width  = 0 ;
    data -> navi_buffer .height = 0 ;
    navi_update( data );
   
    roi_expand_update( data ); moveto_roi_expand( data );

    return ;
  }

// ================================================================================
// expand
//   expand_update
//   expand_x_changed , expand_y_changed
//   expand_width_changed , expand_height_changed
//   expand_scrolled , expand_key_pressed
//   expand_mouse_pressed , expand_mouse_released , expand_mouse_moved
// --------------------------------------------------------------------------------

  static void expand_update(
    my_data * data
  ) {

    // move or update canvas
    if(
      data -> expand_buffer .x <= data -> expand_request .x &&
      data -> expand_buffer .y <= data -> expand_request .y &&
      data -> expand_request .x + data -> expand_request .width  <=
        data -> expand_buffer .x + data -> expand_buffer .width     &&
      data -> expand_request .y + data -> expand_request .height <=
        data -> expand_buffer .y + data -> expand_buffer .height
    ) {
      // move canvas
        gint offset_x ; gint offset_y ;
          offset_x = data -> expand_request .x - data -> expand_buffer .x ;
          offset_y = data -> expand_request .y - data -> expand_buffer .y ;
        gtk_layout_move(
          GTK_LAYOUT( data -> expand_layout ), data -> expand_canvas , - offset_x , - offset_y
        );
    }
    else {
      g_print( "expand update ..." ) ;

      gint image_width ; gint image_height ;
        image_width  = data -> expand_image .width  ;
        image_height = data -> expand_image .height ;

      // update size
        gtk_adjustment_set_upper( data -> expand_hadjust , image_width  );
        gtk_adjustment_set_upper( data -> expand_vadjust , image_height );

      // update page
        gtk_adjustment_set_page_size(      data -> expand_hadjust , data -> expand_request .width  );
        gtk_adjustment_set_page_increment( data -> expand_hadjust , data -> expand_request .width  );
        gtk_adjustment_set_page_size(      data -> expand_vadjust , data -> expand_request .height );
        gtk_adjustment_set_page_increment( data -> expand_vadjust , data -> expand_request .height );

      GtkAllocation buffer ;
        gint margin ;
          margin = 300 ;
        buffer .x      = data -> expand_request .x      - margin ;
        buffer .y      = data -> expand_request .y      - margin ;
        buffer .width  = data -> expand_request .width  + margin ;
        buffer .height = data -> expand_request .height + margin ;

      // adjust alloc
        buffer .x = ( 0 < buffer .x ) ? buffer .x : 0 ;
        buffer .y = ( 0 < buffer .y ) ? buffer .y : 0 ;
        buffer .width  += data -> expand_request .x - buffer .x ;
        buffer .height += data -> expand_request .y - buffer .y ;
        buffer .width  = ( buffer .x + buffer .width  < image_width  ) ? buffer .width  :
                                                                         image_width  - buffer .x ;
        buffer .height = ( buffer .y + buffer .height < image_height ) ? buffer .height :
                                                                         image_height - buffer .y ;

      // update allocation
        data -> expand_buffer .x      = buffer .x      ;
        data -> expand_buffer .y      = buffer .y      ;
        data -> expand_buffer .width  = buffer .width  ;
        data -> expand_buffer .height = buffer .height ;

      // update expand_canvas
        GdkPixbuf * sub_pixbuf ;
          sub_pixbuf = gdk_pixbuf_new_subpixbuf(
            data -> expand_pixbuf , buffer .x , buffer .y , buffer .width , buffer .height
          );
        gint offset_x ; gint offset_y ;
          offset_x = data -> expand_request .x - buffer .x ;
          offset_y = data -> expand_request .y - buffer .y ;

        if( data -> expand_canvas == NULL ) {
          GtkWidget * expand_canvas ;
            expand_canvas = gtk_image_new_from_pixbuf( sub_pixbuf );
          gtk_layout_put( GTK_LAYOUT( data -> expand_layout ), expand_canvas , - offset_x , - offset_y );
          data -> expand_canvas = expand_canvas ;
        }
        else {
          gtk_image_set_from_pixbuf( GTK_IMAGE( data -> expand_canvas ), sub_pixbuf );
          gtk_layout_move(
            GTK_LAYOUT( data -> expand_layout ), data -> expand_canvas , - offset_x , - offset_y
          );
        }
        g_object_unref( sub_pixbuf );

      g_print( " %d %d ", data -> expand_request .x , data -> expand_request .y ) ;
      g_print( " x: %d %d %d", buffer .x , buffer .x + buffer .width  , offset_x ) ;
      g_print( " y: %d %d %d", buffer .y , buffer .y + buffer .height , offset_y ) ;
      g_print( "\n" ) ;
    }

    roi_expand_update( data ); moveto_roi_expand( data );
    return ;
  }


  gboolean expand_x_changed(
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint val ; gint upper ; gint limit ;
      val   = gtk_adjustment_get_value( adjustment );
      upper = gtk_adjustment_get_upper( adjustment );
      limit = upper - data -> expand_request .width ;

    if( limit < val ) {
      val = limit ; gtk_adjustment_set_value( adjustment , val );
    }
    data -> expand_request .x = val ; expand_update( data );

    return FALSE ;
  }


  gboolean expand_y_changed(
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint val ; gint limit ;
      val   = gtk_adjustment_get_value( adjustment );
      limit = gtk_adjustment_get_upper( adjustment ) - data -> expand_request .height ;

    if( limit < val ) {
      val = limit ; gtk_adjustment_set_value( adjustment , val );
    }
    data -> expand_request .y = val ; expand_update( data );

    return FALSE ;
  }


  gboolean expand_width_changed (
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint upper ;
      upper = gtk_adjustment_get_upper( data -> expand_hadjust );
    gint page ;
      page = gtk_adjustment_get_page_size( adjustment );

    if( page == data -> expand_request .width ) { return FALSE ; }
    else if( page <= upper - data -> expand_request .x ) {
      data -> expand_request .width = page ;
      gtk_adjustment_set_page_size(      data -> expand_hadjust , page );
      gtk_adjustment_set_page_increment( data -> expand_hadjust , page );
      expand_update( data );
    }
    else if( page < upper ) {
      data -> expand_request .width = page ;
      gtk_adjustment_set_page_size(      data -> expand_hadjust , page );
      gtk_adjustment_set_page_increment( data -> expand_hadjust , page );
      gtk_adjustment_set_value( data -> expand_hadjust , upper - page );
    }
    else {
      gtk_adjustment_set_page_size(      data -> expand_hadjust , upper );
      gtk_adjustment_set_page_increment( data -> expand_hadjust , upper );
      data -> expand_request .width = upper ;
      if( 0 < data -> expand_request .x ) { gtk_adjustment_set_value( data -> expand_hadjust , 0 ); }
      if( data -> expand_buffer .width < upper ) { expand_update( data ); }
    }

    return FALSE ;
  }


  gboolean expand_height_changed (
    GtkAdjustment * adjustment ,
    my_data       * data
  ) {

    gint upper ;
      upper = gtk_adjustment_get_upper( data -> expand_vadjust );
    gint page ;
      page = gtk_adjustment_get_page_size( adjustment );

    if( page == data -> expand_request .height ) { return FALSE ; }
    else if( page <= upper - data -> expand_request .y ) {
      data -> expand_request .height = page ;
      gtk_adjustment_set_page_size(      data -> expand_vadjust , page );
      gtk_adjustment_set_page_increment( data -> expand_vadjust , page );
      expand_update( data );
    }
    else if( page < upper ) {
      data -> expand_request .height = page ;
      gtk_adjustment_set_page_size(      data -> expand_vadjust , page );
      gtk_adjustment_set_page_increment( data -> expand_vadjust , page );
      gtk_adjustment_set_value( data -> expand_vadjust , upper - page );
    }
    else {
      data -> expand_request .height = upper ;
      gtk_adjustment_set_page_size(      data -> expand_vadjust , upper );
      gtk_adjustment_set_page_increment( data -> expand_vadjust , upper );
      if( 0 < data -> expand_request .y ) { gtk_adjustment_set_value( data -> expand_vadjust , 0 ); }
      if( data -> expand_buffer .height < upper ) { expand_update( data ); }
    }

    return FALSE ;
  }


  gboolean expand_scrolled (
    GtkWidget      * widget ,
    GdkEventScroll * event  ,
    my_data        * data
  ) {

    gint k ; gboolean h ;
    switch( event -> direction ) {
      case GDK_SCROLL_UP    :  k = -1 ; h = FALSE ; break;
      case GDK_SCROLL_DOWN  :  k =  1 ; h = FALSE ; break;
      case GDK_SCROLL_LEFT  :  k = -1 ; h = TRUE  ; break;
      case GDK_SCROLL_RIGHT :  k =  1 ; h = TRUE  ; break;
      default : return FALSE ;
    }

    GtkAdjustment * adjust ;
      adjust = h ? data -> expand_hadjust : data -> expand_vadjust ;

    gint val ; gint step ; gint upper ; gint page ;
      val   = gtk_adjustment_get_value( adjust );
      step  = gtk_adjustment_get_step_increment( adjust );
      upper = gtk_adjustment_get_upper( adjust );
      page  = gtk_adjustment_get_page_size( adjust );

    gint val_new ;
      val_new = val + k * 4 * step ;
      val_new = ( val_new < upper - page ) ? val_new : upper - page ;
      gtk_adjustment_set_value( adjust , val_new );

    return FALSE ;
  }


  gboolean expand_key_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {

    gint p ; gint q ; gint r ; gint s ; gboolean h ;
        p = 0 ; q = 0 ; r = 1 ; s = 0 ; h = FALSE ;
    switch( event -> keyval ) {
      case 0xff50 : r =  0 ; break ; // home
      case 0xff57 : q =  1 ; break ; // end
      case 0xff55 : p = -1 ; break ; // page up
      case 0xff56 : p =  1 ; break ; // page down
      case 0xff51 : s = -1 ; h = TRUE ; break ; // ←
      case 0xff52 : s = -1 ;            break ; // ↑
      case 0xff53 : s =  1 ; h = TRUE ; break ; // →
      case 0xff54 : s =  1 ;            break ; // ↓
      default : /*
        g_print( "key: %04x - %04x%s%s%s%s\n",
          event -> keyval ,
          event -> state ,
          event -> state & GDK_SHIFT_MASK        ? " shift"   : "" ,
          event -> state & GDK_CONTROL_MASK      ? " control" : "" ,
          event -> state & GDK_MOD2_MASK         ? " command" : "" ,
          event -> state & GDK_BUTTON5_MASK << 1 ? " option"  : ""
        ) ; // */
        return FALSE ;
    }

    // scroll expand
      if( event -> state & GDK_SHIFT_MASK ) { return FALSE ; }

      GtkAdjustment * adjust ;
        adjust = ( s == 0 && event -> state & GDK_CONTROL_MASK || h ) ?
                   data -> expand_hadjust : data -> expand_vadjust ;
      gint val ; gint step ; gint page ; gint upper ;
        val   = gtk_adjustment_get_value( adjust );
        step  = gtk_adjustment_get_step_increment( adjust );
        page  = gtk_adjustment_get_page_increment( adjust );
        upper = gtk_adjustment_get_upper( adjust );
      gint d ;
        d = ( event -> state & GDK_CONTROL_MASK ) ? 9 * step : 1 ;
 
      gint val_new ;
        val_new = p * page + q * upper + r * val + s * d ;
        val_new = ( val_new < 0 ) ? 0 : ( val_new < upper - page ) ? val_new : upper - page ;
      gtk_adjustment_set_value( adjust , val_new );

    return FALSE ;
  }


  gboolean expand_mouse_pressed (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    // save mouse event type
      data -> mouse_eventtype = event -> type ;

    // save mouse position
      data -> mouse_posi .x = event -> x ;
      data -> mouse_posi .y = event -> y ;

    GdkCursorType type ;

    if( event -> type == GDK_BUTTON_PRESS &&
        data -> expand_cursor == GDK_BLANK_CURSOR ) { // first single click

      type = GDK_FLEUR ;

      // change cursor
        data -> mouse_cursor = gdk_window_get_cursor( gtk_widget_get_window( widget ) );
        data -> expand_cursor  = type ;
        gdk_window_set_cursor(
          gtk_widget_get_window( widget ), gdk_cursor_new( data -> expand_cursor )
        );

      // save adjust value
        data -> expand_adjust_pressed .x = gtk_adjustment_get_value( data -> expand_hadjust );
        data -> expand_adjust_pressed .y = gtk_adjustment_get_value( data -> expand_vadjust );

      // save mouse position
        data -> expand_mouse_pressed .x = event -> x ;
        data -> expand_mouse_pressed .y = event -> y ;

    }

    return FALSE ;
  }


  gboolean expand_mouse_released (
    GtkWidget      * widget ,
    GdkEventButton * event  ,
    my_data        * data
  ) {

    // restore mouse cursor
      if( data -> expand_cursor != GDK_BLANK_CURSOR ) {
        gdk_window_set_cursor( gtk_widget_get_window( widget ), data -> mouse_cursor );
        data -> mouse_cursor = NULL ;
        data -> expand_cursor = GDK_BLANK_CURSOR ;
      }

    return FALSE ;
  }


  gboolean expand_mouse_moved (
    GtkWidget      * widget ,
    GdkEventMotion * event  ,
    my_data        * data
  ) {

    if( data -> expand_cursor == GDK_BLANK_CURSOR ) { return FALSE ; }

    // mouse move
      gint move_x ; gint move_y ;
        move_x = (gint) event -> x - data -> mouse_posi .x ;
        move_y = (gint) event -> y - data -> mouse_posi .y ;
      gint d ;
        d = 3 ;

    if( - d < move_x && move_x < d && - d < move_y && move_y < d ) {} else { // mouse moved

      gint mouse_x ; gint mouse_y ;
        GdkWindow * window ;
          window = gtk_widget_get_window( data -> expand_window );
        gint ox ; gint oy ;
          gdk_window_get_position( window , & ox , & oy );
        GdkDisplay * display ;
          display = gdk_window_get_display( window );
        gdk_display_get_pointer( display , NULL , & mouse_x , & mouse_y , NULL );
          mouse_x -= ox ;
          mouse_y -= oy ;

      if( data -> expand_cursor == GDK_FLEUR ) { // drag expand
        gint x ; gint y ;
          x = data -> expand_adjust_pressed .x - ( mouse_x - data -> expand_mouse_pressed .x );
          y = data -> expand_adjust_pressed .y - ( mouse_y - data -> expand_mouse_pressed .y );
          gint x_max ; gint y_max ;
            x_max = data -> expand_image .width  - data -> expand_request .width  ;
            y_max = data -> expand_image .height - data -> expand_request .height ;
          x = ( x < x_max ) ? x : x_max ;
          y = ( y < y_max ) ? y : y_max ;
        gtk_adjustment_set_value( data -> expand_hadjust , x );
        gtk_adjustment_set_value( data -> expand_vadjust , y );
      }

      // save mouse position
        data -> mouse_posi .x = mouse_x ;
        data -> mouse_posi .y = mouse_y ;

    }

    return FALSE ;
  }

// ================================================================================
// tab_pressed
// --------------------------------------------------------------------------------

  gboolean tab_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {
    GtkWidget * window ;
      window = ( widget == data -> navi_window   ) ? data -> expand_window :
               ( widget == data -> expand_window ) ? data -> navi_window   :
                                                     NULL ;
    switch( event -> keyval ) {
      case 0xff09 : // tab
        if( window != NULL ) { gtk_window_present( GTK_WINDOW( window ) ); }
        break ;
      default : return FALSE ;
    }
    return TRUE ;
  }

// ================================================================================
// roi_expand
//   roi_expand_update , moveto_roi_expand
//   on_roi_expand
//   roi_expand_key_pressed
// --------------------------------------------------------------------------------

  void roi_expand_update(
    my_data * data
  ) {

    gint request_width ; gint request_height ; gint request_x ; gint request_y ;
      gdouble k ;
        k = (gdouble) data -> navi_image .width / data -> expand_image .width ;
      request_width  = k * data -> expand_request .width  ;
      request_height = k * data -> expand_request .height ;
      request_x = k * data -> expand_request .x  ;
      request_y = k * data -> expand_request .y ;

    gint roi_width ; gint roi_height ;
      roi_width  = 0 ;
      roi_height = 0 ;
      if( data -> roi_expand_canvas != NULL ) {
        roi_width  = data -> roi_expand_canvas -> allocation .width  ;
        roi_height = data -> roi_expand_canvas -> allocation .height ;
      }

    gint offset_x ; gint offset_y ;
      offset_x = request_x - data -> navi_request .x ;
      offset_y = request_y - data -> navi_request .y ;

    guint32 color ;
      color = data -> roi_expand_selected ? 0xff000060 : 0x00ff0060 ;

    // move or update canvas
    if( color == data -> roi_expand_selected &&
        request_width == roi_width && request_height == roi_height
    ) {
      // move canvas
        // g_print( "roi moved\n" );
        gtk_layout_move(
          GTK_LAYOUT( data -> navi_layout ), data -> roi_expand_canvas , offset_x , offset_y
        );
    }
    else {
      // upate canvas
        // g_print( "roi update %d %d\n", request_width , request_height );
        GdkPixbuf * roi_pixbuf ;
          roi_pixbuf = gdk_pixbuf_new(
            GDK_COLORSPACE_RGB , TRUE , 8 , request_width , request_height
          );
          data -> roi_expand_color = color ;
          gdk_pixbuf_fill( roi_pixbuf , color );

        if( data -> roi_expand_canvas == NULL ) {
          GtkWidget * roi_canvas ;
            roi_canvas = gtk_image_new_from_pixbuf( roi_pixbuf );
          gtk_layout_put(
            GTK_LAYOUT( data -> navi_layout ), roi_canvas , offset_x , offset_y
          );
          data -> roi_expand_canvas = roi_canvas ;
        }
        else {
          gtk_image_set_from_pixbuf( GTK_IMAGE( data -> roi_expand_canvas ), roi_pixbuf );
          gtk_layout_move(
            GTK_LAYOUT( data -> navi_layout ), data -> roi_expand_canvas , offset_x , offset_y
          );
        }
        g_object_unref( roi_pixbuf );
    }

    return ;
  }


  void moveto_roi_expand(
    my_data * data
  ) {

    gint roi_width ; gint roi_height ; gint roi_x ; gint roi_y ;
      gdouble k ;
        k = (gdouble) data -> navi_image .width / data -> expand_image .width ;
      roi_width  = (gint) ( k * data -> expand_request .width  + 0.5 );
      roi_height = (gint) ( k * data -> expand_request .height + 0.5 );
      roi_x      = (gint) ( k * data -> expand_request .x      + 0.5 );
      roi_y      = (gint) ( k * data -> expand_request .y      + 0.5 );

    gint roi_right ; gint roi_bottom ;
      roi_right  = roi_x + roi_width ;
      roi_bottom = roi_y + roi_height ;

    gint x ; gint width ; gint y ; gint height ;
      x      = (gint) ( gtk_adjustment_get_value(     data -> navi_hadjust ) + 0.5 );
      width  = (gint) ( gtk_adjustment_get_page_size( data -> navi_hadjust ) + 0.5 );
      y      = (gint) ( gtk_adjustment_get_value(     data -> navi_vadjust ) + 0.5 );
      height = (gint) ( gtk_adjustment_get_page_size( data -> navi_vadjust ) + 0.5 );

    gint right ; gint bottom ;
      right  = x + width ;
      bottom = y + height ;

    gint x_new ; gint y_new ;
      x_new = ( roi_x     < x )               ? roi_x :
              ( roi_right < right )           ? x     :
              ( width     < roi_width )       ? x     :
              ( width     < roi_right )       ? roi_right  - width  :
                                                x     ;
      y_new = ( roi_y      < y )              ? roi_y :
              ( roi_bottom < bottom )         ? y     :
              ( height     < roi_height )     ? y     :
              ( height     < roi_bottom )     ? roi_bottom - height :
                                                y     ;

    // if( x_new != x || y_new != y ) { g_print( "navi scrolled\n" ); }
    if( x_new != x ) { gtk_adjustment_set_value( data -> navi_hadjust , x_new ) ; }
    if( y_new != y ) { gtk_adjustment_set_value( data -> navi_vadjust , y_new ) ; }

    return ;
  }


  gboolean on_roi_expand (
    gint            event_x ,
    gint            event_y ,
    GdkCursorType   type ,
    my_data       * data
  ) {

    gint roi_width ; gint roi_height ; gint roi_x ; gint roi_y ;
      gdouble k ;
        k = (gdouble) data -> navi_image .width / data -> expand_image .width ;
      roi_width  = (gint) ( k * data -> expand_request .width  + 0.5 );
      roi_height = (gint) ( k * data -> expand_request .height + 0.5 );
      roi_x      = (gint) ( k * data -> expand_request .x      + 0.5 );
      roi_y      = (gint) ( k * data -> expand_request .y      + 0.5 );

    gint roi_right ; gint roi_bottom ;
      roi_right  = roi_x + roi_width ;
      roi_bottom = roi_y + roi_height ;

    gint x ; gint width ; gint y ; gint height ;
      x      = (gint) ( gtk_adjustment_get_value( data -> navi_hadjust ) + event_x + 0.5 );
      y      = (gint) ( gtk_adjustment_get_value( data -> navi_vadjust ) + event_y + 0.5 );

    gint d_in ; gint d_out ;
      d_in = 5 , d_out =5 ;

    gboolean result ;
      switch( type ) {
        case GDK_RIGHT_SIDE :
          result = ( roi_right  - d_in < x && x < roi_right  + d_out );
          break ;
        case GDK_BOTTOM_SIDE :
          result = ( roi_bottom - d_in < y && y < roi_bottom + d_out );
          break ;
        case GDK_BOTTOM_RIGHT_CORNER :
          result = (
            roi_right  - d_in < x && x < roi_right  + d_out &&
            roi_bottom - d_in < y && y < roi_bottom + d_out
          );
          break ;
        case GDK_FLEUR :
          result = ( roi_x < x && x < roi_right  && roi_y < y && y < roi_bottom );
          break ;
        default :
          result = FALSE ;
      }

    return result ;
  }


  gboolean roi_expand_key_pressed (
    GtkWidget   * widget ,
    GdkEventKey * event  ,
    my_data     * data
  ) {

    gint p ; gint q ; gint r ; gint s ; gboolean h ;
        p = 0 ; q = 0 ; r = 1 ; s = 0 ; h = FALSE ;
    switch( event -> keyval ) {
      case 0xff50 : r =  0 ; break ; // home
      case 0xff57 : q =  1 ; break ; // end
      case 0xff55 : p = -1 ; break ; // page up
      case 0xff56 : p =  1 ; break ; // page down
      case 0xff51 : s = -1 ; h = TRUE ; break ; // ←
      case 0xff52 : s = -1 ;            break ; // ↑
      case 0xff53 : s =  1 ; h = TRUE ; break ; // →
      case 0xff54 : s =  1 ;            break ; // ↓
      default : /*
        g_print( "key: %04x - %04x%s%s%s%s\n",
          event -> keyval ,
          event -> state ,
          event -> state & GDK_SHIFT_MASK        ? " shift"   : "" ,
          event -> state & GDK_CONTROL_MASK      ? " control" : "" ,
          event -> state & GDK_MOD2_MASK         ? " command" : "" ,
          event -> state & GDK_BUTTON5_MASK << 1 ? " option"  : ""
        ) ; // */
        return FALSE ;
    }

    // scroll expand
      if( event -> state & GDK_SHIFT_MASK             ) { return FALSE ; }
      if( event -> state & GDK_MOD2_MASK              ) { return FALSE ; }
      if( event -> state & GDK_BUTTON5_MASK << 1      ) { return FALSE ; }
      if( event -> state & GDK_CONTROL_MASK && s != 0 ) { return FALSE ; }
      if( p != 0 ) {
        h = ! ( event -> state & GDK_CONTROL_MASK ) ;
        s = p ;
      }

      GtkAdjustment * adjust ; gint d ;
      //
        adjust = h ? data -> expand_hadjust : data -> expand_vadjust ;
        d = h ? data -> expand_request .width : data -> expand_request .height ;
      if( s != 0 ) {
        gint val ; gint upper ; gint page ;
          val   = gtk_adjustment_get_value( adjust );
          upper = gtk_adjustment_get_upper( adjust );
          page  = gtk_adjustment_get_page_increment( adjust );
        gint val_new ;
          val_new = val + s * d ;
          val_new = ( val_new < 0 ) ? 0 : ( val_new < upper - page ) ? val_new : upper - page ;
        gtk_adjustment_set_value( adjust , val_new );
      }
      //
        h = event -> state & GDK_CONTROL_MASK ;
        adjust = h ? data -> expand_hadjust : data -> expand_vadjust ;
        d = h ? data -> expand_request .width : data -> expand_request .height ;
      if( r == 0 || p != 0 ) {
        gtk_adjustment_set_value( adjust , 0 );
      }
      if( q == 1 ) {
        gint upper ;
          upper = gtk_adjustment_get_upper( adjust );
        gtk_adjustment_set_value( adjust , upper - data -> expand_request .height );
      }

    return TRUE ;
  }

// ================================================================================
// image
//   expand_image_update , navi_image_update
// --------------------------------------------------------------------------------

  void expand_image_update(
    my_data * data
  ) {

    g_print( "full image ..." ) ;

    data -> expand_pixbuf = NULL ;
    data -> expand_image .width  = 0 ;
    data -> expand_image .height = 0 ;

    std::string file_name ;
      if( data -> argc < 2 ) { g_print( "no file name\n" ) ; return ; }
      file_name = data -> argv[1] ;

    GdkPixbuf * full_image ;
      full_image = gdk_pixbuf_new_from_file( file_name .c_str() , NULL );
      if( ! full_image ) { g_print( "cannot read\n" ); return ; }

    gint full_width ; gint full_height ;
      full_width  = gdk_pixbuf_get_width(  full_image );
      full_height = gdk_pixbuf_get_height( full_image );
      g_print( " w:%d h:%d\n", full_width , full_height ) ;

    data -> expand_pixbuf = full_image ;
    data -> expand_image .width  = full_width  ;
    data -> expand_image .height = full_height ;

    return ;
  }


  void navi_image_update(
    gint      request_width ,
    my_data * data
  ) {

    g_print( "navi image ..." ) ;

    GdkPixbuf * full_image ; gint full_width ; gint full_height ;
      full_image  = data -> expand_pixbuf ;
      full_width  = data -> expand_image .width  ;
      full_height = data -> expand_image .height ;

    gint scaled_width ; gint scaled_height ;
      gdouble k ;
        k = (gdouble) request_width / full_width ;
        if( 1.0 < k ) { k = 1.0 ; }
      scaled_width  = gint( k * full_width  + 0.5 );
      scaled_height = gint( k * full_height + 0.5 );
      g_print( " r:%d k=%.2f w:%d h:%d\n",  request_width , k , scaled_width , scaled_height ) ;

    GdkPixbuf * scaled_image ;
      scaled_image = gdk_pixbuf_scale_simple(
        full_image , scaled_width , scaled_height , GDK_INTERP_BILINEAR
      );
      if( scaled_image == NULL ) { return ; }

    if( data -> navi_pixbuf != NULL ) { g_object_unref( data -> navi_pixbuf ); }
    data -> navi_pixbuf = scaled_image ;
    data -> navi_image .width  = scaled_width  ;
    data -> navi_image .height = scaled_height ;

    return ;
  }

// ================================================================================
// main
// --------------------------------------------------------------------------------

int main(
  int     argc ,
  char ** argv
) {

  gtk_init( & argc , & argv );
    g_print( "--- %s message ---\n", argv[0] ) ;

  struct my_data data ; {
    data .argc = argc ;
    data .argv = argv ;

    data .navi_canvas = NULL ;
    data .navi_buffer .x = 0 ;
    data .navi_buffer .y = 0 ;
    data .navi_buffer .width  = 0 ;
    data .navi_buffer .height = 0 ;
    data .navi_request .x = 0 ;
    data .navi_request .y = 0 ;
    data .navi_request .width  = 800 ;
    data .navi_request .height = 700 ;

    data .navi_cursor = GDK_BLANK_CURSOR ;

    data .expand_canvas = NULL ;
    data .expand_buffer .x = 0 ;
    data .expand_buffer .y = 0 ;
    data .expand_buffer .width  = 0 ;
    data .expand_buffer .height = 0 ;
    data .expand_request .x = 0 ;
    data .expand_request .y = 0 ;
    data .expand_request .width  = 300 ;
    data .expand_request .height = 340 ;

    data .expand_cursor = GDK_BLANK_CURSOR ;

    data .roi_expand_canvas = NULL ;
    data .roi_expand_selected = TRUE ;

    data .mouse_posi .x = 0 ;
    data .mouse_posi .y = 0 ;
    data .mouse_cursor = NULL ;
    data .mouse_eventtype = GDK_NOTHING ;

  }

  GtkWidget * navi_window ; {

    GtkWidget * navi_layout ;
      navi_layout = gtk_layout_new( NULL , NULL );
      gtk_widget_set_redraw_on_allocate( navi_layout , FALSE );
      gtk_widget_add_events(
        navi_layout ,
        GDK_BUTTON_PRESS_MASK   | GDK_BUTTON_RELEASE_MASK      |
        GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK
      );
      gtk_widget_set_can_focus( navi_layout , TRUE );
      my_signal_connect( navi_layout , "key-press-event"      , navi_key_pressed    );
      my_signal_connect( navi_layout , "button-press-event"   , navi_mouse_pressed  );
      my_signal_connect( navi_layout , "button-release-event" , navi_mouse_released );
      my_signal_connect( navi_layout , "motion-notify-event"  , navi_mouse_moved );
      my_signal_connect( navi_layout , "scroll-event"         , navi_scrolled    );

    GtkAdjustment * layout_width ; GtkAdjustment * layout_height ;
      layout_width  = gtk_layout_get_hadjustment( GTK_LAYOUT( navi_layout ) );
      layout_height = gtk_layout_get_vadjustment( GTK_LAYOUT( navi_layout ) );
      my_signal_connect( layout_width  , "changed" , navi_width_changed  );
      my_signal_connect( layout_height , "changed" , navi_height_changed );

    GtkAdjustment * navi_hadjust ; GtkAdjustment * navi_vadjust ;
      gint page_w ; gint page_h ;
        page_w = navi_layout -> allocation .width  ;
        page_h = navi_layout -> allocation .height ;
      navi_hadjust = (GtkAdjustment*) gtk_adjustment_new( 0 , 0 , page_w , 10 , page_w , page_w );
      navi_vadjust = (GtkAdjustment*) gtk_adjustment_new( 0 , 0 , page_h , 10 , page_h , page_h );
      my_signal_connect( navi_hadjust , "value-changed" , navi_x_changed );
      my_signal_connect( navi_vadjust , "value-changed" , navi_y_changed );

    navi_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
      GtkAllocation default_size ;
        default_size .width  = data .navi_request .width  ;
        default_size .height = data .navi_request .height ;
        gtk_window_set_default_size( GTK_WINDOW( navi_window ),
           default_size .width , default_size .height
        );
      GtkAllocation request_size ;
        gdouble k ;
          k = 0.6 ;
        request_size .width  = (gint)( k * default_size .width  + 0.5 );
        request_size .height = (gint)( k * default_size .height + 0.5 );
        gtk_widget_set_size_request( navi_window ,
          request_size .width , request_size .height
        );
      gtk_window_move( GTK_WINDOW( navi_window ), 0 , 0 );
      my_signal_connect( navi_window , "key-press-event" , tab_pressed );
      g_signal_connect( G_OBJECT( navi_window ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );

    GtkWidget * navi_vbox ; GtkWidget * navi_hbox ;
      navi_vbox = gtk_vbox_new( FALSE , 0 );
      navi_hbox = gtk_hbox_new( FALSE , 0 );

    GtkWidget * navi_hscrollbar ; GtkWidget * navi_vscrollbar ;
      navi_hscrollbar = gtk_hscrollbar_new( navi_hadjust );
      navi_vscrollbar = gtk_vscrollbar_new( navi_vadjust );

    GtkWidget * navi_scale ; // scale button
      navi_scale = gtk_hbox_new( FALSE , 0 );

    GtkWidget * scale_button[5] ; // buttons
      #define navi_scale_add( col , label ) ( \
        scale_button[ col ] = gtk_button_new_with_label( label ), \
        gtk_box_pack_start( GTK_BOX( navi_scale ), scale_button[ col ], FALSE, FALSE, 2 ), \
        my_signal_connect( scale_button[ col ], "clicked", navi_scale_clicked ) \
      )
      navi_scale_add( 0, "＋" ) ;
      navi_scale_add( 1, "ー" ) ;
      navi_scale_add( 2, "⇕" ) ;
      navi_scale_add( 3, "⇔" ) ;

    gtk_container_add( GTK_CONTAINER( navi_window ) , navi_vbox );
    gtk_container_add( GTK_CONTAINER( navi_vbox )   , navi_hbox );
    gtk_box_pack_start( GTK_BOX( navi_hbox ) , navi_layout     , TRUE  , TRUE  , 0 );
    gtk_box_pack_end(   GTK_BOX( navi_hbox ) , navi_vscrollbar , FALSE , FALSE , 0 );
    gtk_box_pack_start( GTK_BOX( navi_vbox ) , navi_hscrollbar , FALSE , FALSE , 0 );
    gtk_box_pack_end(   GTK_BOX( navi_vbox ) , navi_scale      , FALSE , FALSE , 2 );

    data .navi_layout  = navi_layout  ;
    data .navi_hadjust = navi_hadjust ;
    data .navi_vadjust = navi_vadjust ;
    data .navi_window  = navi_window  ;
    data .navi_hbox    = navi_hbox    ;
  }

  GtkWidget * expand_window ; {

    GtkWidget * expand_layout ;
      expand_layout = gtk_layout_new( NULL , NULL );
      gtk_widget_set_redraw_on_allocate( expand_layout , FALSE );
      gtk_widget_add_events(
        expand_layout ,
        GDK_BUTTON_PRESS_MASK   | GDK_BUTTON_RELEASE_MASK      |
        GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK
      );
      my_signal_connect( expand_layout , "button-press-event"   , expand_mouse_pressed  );
      my_signal_connect( expand_layout , "button-release-event" , expand_mouse_released );
      my_signal_connect( expand_layout , "motion-notify-event"  , expand_mouse_moved );
      my_signal_connect( expand_layout , "scroll-event"         , expand_scrolled    );

    GtkAdjustment * layout_width ; GtkAdjustment * layout_height ;
      layout_width  = gtk_layout_get_hadjustment( GTK_LAYOUT( expand_layout ) );
      layout_height = gtk_layout_get_vadjustment( GTK_LAYOUT( expand_layout ) );
      my_signal_connect( layout_width  , "changed" , expand_width_changed  );
      my_signal_connect( layout_height , "changed" , expand_height_changed );

    GtkAdjustment * expand_hadjust ; GtkAdjustment * expand_vadjust ;
      gint page_w ; gint page_h ;
        page_w = expand_layout -> allocation .width  ;
        page_h = expand_layout -> allocation .height ;
      expand_hadjust = (GtkAdjustment*) gtk_adjustment_new( 0 , 0 , page_w , 10 , page_w , page_w );
      expand_vadjust = (GtkAdjustment*) gtk_adjustment_new( 0 , 0 , page_h , 10 , page_h , page_h );
      my_signal_connect( expand_hadjust , "value-changed" , expand_x_changed );
      my_signal_connect( expand_vadjust , "value-changed" , expand_y_changed );

    expand_window = gtk_window_new( GTK_WINDOW_TOPLEVEL );
      gtk_window_set_title( GTK_WINDOW( expand_window ), "expand" );
      GtkAllocation default_size ;
        default_size .width  = data .expand_request .width  ;
        default_size .height = data .expand_request .height ;
      gtk_window_set_default_size( GTK_WINDOW( expand_window ),
         default_size .width , default_size .height
      );
      gtk_window_set_position( GTK_WINDOW( expand_window ), GTK_WIN_POS_CENTER );
      my_signal_connect( expand_window , "key-press-event" , tab_pressed );
      my_signal_connect( expand_window , "key-press-event" , expand_key_pressed );
      g_signal_connect( G_OBJECT( expand_window ), "destroy", G_CALLBACK( gtk_main_quit ), NULL );

    GtkWidget * expand_vbox ; GtkWidget * expand_hbox ;
      expand_vbox = gtk_vbox_new( FALSE , 0 );
      expand_hbox = gtk_hbox_new( FALSE , 0 );

    GtkWidget * expand_hscrollbar ; GtkWidget * expand_vscrollbar ;
      expand_hscrollbar = gtk_hscrollbar_new( expand_hadjust );
      expand_vscrollbar = gtk_vscrollbar_new( expand_vadjust );

    gtk_container_add( GTK_CONTAINER( expand_window ) , expand_vbox );
    gtk_container_add( GTK_CONTAINER( expand_vbox )   , expand_hbox );
    gtk_box_pack_start( GTK_BOX( expand_hbox ) , expand_layout     , TRUE  , TRUE  , 0 );
    gtk_box_pack_end(   GTK_BOX( expand_hbox ) , expand_vscrollbar , FALSE , FALSE , 0 );
    gtk_box_pack_start( GTK_BOX( expand_vbox ) , expand_hscrollbar , FALSE , FALSE , 0 );

    data .expand_layout  = expand_layout  ;
    data .expand_hadjust = expand_hadjust ;
    data .expand_vadjust = expand_vadjust ;
    data .expand_window  = expand_window  ;
  }

  expand_image_update( & data );
    if( data .expand_pixbuf == NULL ) { return -1 ; }
  navi_image_update( 1200 , & data );
    if( data .navi_pixbuf == NULL ) { return -1 ; }

  // adjust navi_request
    gint scaled_width ; gint scaled_height ;
      scaled_width  = data .navi_image .width  ;
      scaled_height = data .navi_image .height ;
    if( scaled_width  < data .navi_request .width  ) { data .navi_request .width  = scaled_width  ; }
    if( scaled_height < data .navi_request .height ) { data .navi_request .height = scaled_height ; }

  // adjust expand_request
    gint w ; gint h ;
      w = gint( (gdouble) data .expand_image .width  / 4 + 0.5 );
      h = gint( (gdouble) data .expand_image .height / 5 + 0.5 );
    gtk_window_resize( GTK_WINDOW( data .expand_window ), w , h );

  navi_update( & data );
  expand_update( & data );
  roi_expand_update( & data );

  gtk_widget_show_all( expand_window );
  gtk_widget_show_all( navi_window );
  gtk_main( );

  g_object_unref( data .expand_pixbuf );
  g_object_unref( data .navi_pixbuf );
  return 0 ;
}

